﻿using System;
using Blackbird.Common.Exceptions;
using Blackbird.Common.Properties;

namespace Blackbird.Common {
    /// <summary>
    /// Represents a generically-typed container with one or more dimensions
    /// </summary>
    /// <typeparam name="T">The type of object that the graph holds.</typeparam>
    public abstract class Graph<T> {
        # region Private Fields

        private int[] _dimensionExtents;

        protected int[] DimensionExtents
        {
            get { return _dimensionExtents; }
            set
            {
                Configured = false;
                _dimensionExtents = value;
            }
        }

        #endregion

        #region Properties

        public bool Configured { get; set; }
        public int NumberOfDimensions { get; set; }

        public ulong MaximumItemCount {
            get { return GetMaximumItemCount(); }
        }


        /// <summary>
        /// Gets or sets the item at the given coordinates.
        /// </summary>
        /// <param name="dimensionIndexes">The coordinates that identify the item or location on which to operate.</param>
        /// <returns>The item at the given coordinates.</returns>
        public T this[params int[] dimensionIndexes]
        {
            get
            {
                if (!Configured)
                    Configure();
                return GetItemByIndex(GetLinearIndexFromCoordinates(dimensionIndexes));
            }
            set
            {
                if(!Configured)
                    Configure();
                SetItemByIndex(value, GetLinearIndexFromCoordinates(dimensionIndexes));
            }
        }

        #endregion

        #region Constructors

        protected Graph() {
        }

        /// <summary>
        /// Constructs a new instance of a Graph with the given type in two dimensions.
        /// </summary>
        /// <param name="xSize">The number of elements in the x axis</param>
        /// <param name="ySize">The number of elements in the y axis</param>
        protected Graph(int xSize, int ySize)
        {
            DimensionExtents = new[] { xSize, ySize };
        }

        /// <summary>
        /// Constructs a new instance of a Graph with the given type in three dimensions.
        /// </summary>
        /// <param name="xSize">The number of elements in the x axis.</param>
        /// <param name="ySize">The number of elements in the y axis.</param>
        /// <param name="zSize">The number of elements in the z axis.</param>
        protected Graph(int xSize, int ySize, int zSize) {
            DimensionExtents = new[] { xSize, ySize, zSize };
        }

        /// <summary>
        /// Constructs a new instance of a graph with the given type in the given dimensions.
        /// </summary>
        /// <param name="dimensionExtents">Each parameter value represents the number of elements in the given dimension.</param>
        protected Graph(params int[] dimensionExtents)
        {
            DimensionExtents = dimensionExtents;
        }

        #endregion

        #region Public Methods

        public int GetDimensionExtent(int dimensionIndex) {
            return DimensionExtents[dimensionIndex];
        }

        public T[] ExtractColumn(int dimensionIndex, params int[] columnIndexes) {
            if ((columnIndexes.Length + 1) != NumberOfDimensions)
                throw new ArgumentException(Resources.ColumnIndexCountMismatchMessage,
                                            "columnIndexes");

            if ((dimensionIndex < 0) || (dimensionIndex > NumberOfDimensions))
                throw new ArgumentException(Resources.InvalidDimensionValueExceptionMessage, "dimensionIndex");

            var columnValues = new T[GetDimensionExtent(dimensionIndex)];


            var selectorIndexes = new int[NumberOfDimensions];
            for (int i = 0; i < columnValues.Length; i++) {
                selectorIndexes[dimensionIndex] = i;
                for (int p = 0; p < NumberOfDimensions; p++) {
                    if (p < dimensionIndex)
                        selectorIndexes[p] = columnIndexes[p];
                    else if (p == dimensionIndex)
                        selectorIndexes[p] = i;
                    else if (p > dimensionIndex)
                        selectorIndexes[p] = columnIndexes[p - 1];
                }
                columnValues[i] = this[selectorIndexes];
            }

            return columnValues;
        }

        #endregion

        #region Protected Methods

        protected internal abstract T GetItemByIndex(ulong index);
        protected internal abstract void SetItemByIndex(T item, ulong index);

        protected ulong GetLinearIndexFromCoordinates(params int[] dimensionIndexes) {
            if (!Configured)
                MapNotInitializedException.ThrowStandardException();

            if (dimensionIndexes.Length != NumberOfDimensions)
                throw new ArgumentException(Resources.InvalidCoordinateNumberMessage, "dimensionIndexes");

            ulong listIndex = 0;
            for (int i = 0; i < dimensionIndexes.Length; i++) {
                if ((dimensionIndexes[i] < 0) || (dimensionIndexes[i] > DimensionExtents[i]))
                    throw new ArgumentException(Resources.DimensionCoordinatesOutOfBoundsMessage, "dimensionIndexes");
                listIndex += ((ulong)dimensionIndexes[i] * CalcDimensionalOffset(i));
            }

            return listIndex;
        }

        protected ulong CalcDimensionalOffset(int dimensionIndex) {
            ulong result = 1;
            for (int i = 0; i < dimensionIndex; i++)
                result *= (ulong)DimensionExtents[dimensionIndex];
            return result;
        }

        public abstract void InitializeStorage();
        public virtual void Initialize() { }

        public void Configure() {
            Initialize();

            NumberOfDimensions = _dimensionExtents.Length;
            
            if (NumberOfDimensions < 1)
                throw new ArgumentException(Resources.InvalidGraphDimensionCountMessage);
            
            
            InitializeStorage();

            Configured = true;
        }

        #endregion

        #region Private Methods

        private ulong GetMaximumItemCount() {
            ulong max = 1;

            for (int i = 0; i < NumberOfDimensions; i++)
                max *= (ulong)DimensionExtents[i];

            return max;
        }

        #endregion

    }
}